概要
本文介紹使用 Unreal Insight 進行優化負荷調查的流程,以及如何使用 C++ 實現非同步加載(預先讀取)以防止在遊戲內使用 LoadSynchronous()
同步加載素材時發生的停頓。關於 Blueprint 的非同步加載,請參考下面的參考連結。
環境
- Rider 2024.2.6
- Unreal Engine 5.4
- Windows 11 Pro
參考資料
- 官方文檔 Asynchronous Asset Loading
- [UE4] Asset Manager 的素材非同步加載功能 第一部分 (非同步加載的解釋及關卡的預讀)
- Maximizing Your Game's Performance in Unreal Engine | Unreal Fest 2022
- 特別是 22:40 到 28:15 之間提供了有關非同步加載的信息,在 27:33 介紹了使用 Blueprint 進行非同步加載(AsyncLoad)的實現方法。
- [UE5] 嘗試使用 Unreal Insights
- Unreal Insights
本文
使用 Unreal Insight 進行優化的負荷調查
Unreal Insights 是什麼?
Unreal Insights 是一個用於分析 Unreal Engine 中性能和內存使用情況的工具。官方文檔在這裡: https://dev.epicgames.com/documentation/en-us/unreal-engine/unreal-insights-in-unreal-engine
使用方法
有關詳細的使用方法,請參考 [UE5] 嘗試使用 Unreal Insights。
進行優化調查時,理想情況下應使用目標平台的Package版本,而非普通的 PIE(在編輯器中播放)模式。因為在 PIE 模式下,預先加載的緩存和背景操作會影響,導致難以進行準確的負荷測量。
這次我們將在接近Package版本的 Standalone Game 中進行調查。
如何啟動 Standalone Game
如下面的圖片所示,從 UE 編輯器啟動 Standalone Game。
如果在遊玩過程中發生了卡頓,可以使用以下命令。
stat unitgraph
「stat UnitGraph」命令是用來可視化遊戲處理負荷的工具,以圖表的形式顯示。這樣一來,在發生停頓的時候,圖表上會出現明顯的尖峰,能夠清楚地識別問題所在。
與許多人常用的「stat Unit」或「stat fps」不同,「stat UnitGraph」能夠記錄短暫的停頓,較少漏掉。
執行後會顯示如下圖表。左下角顯示負荷圖,右上角顯示具體數據。若以 60fps 為目標,理想的情況下幀時間應保持在16.6ms以下。
接下來,我們將使用 Unreal Insights 進行實際測量。
trace.start
和 trace.stop
要開始使用 Unreal Insights 進行測量,請執行 trace.start
命令。測量完成後,使用 trace.stop
結束。
處理負荷測量結果
如下面的視頻所示,確認到卡頓的發生。
當重處理執行時,圖表上會顯示出明顯的變化。
遊戲: 58.26ms
可以看到遊戲線程耗時 58.26ms。
接下來,我們也來檢查一下 Unreal Insights 的測量結果。
如何打開 Trace
打開 Trace 數據後,會顯示如下結果。
在這個結果中,值得關注的是綠色條所示的
LoadObject (154.7ms) - /Game/Main/InGame/VFX/Niagara/NS_DizzyStar.NS_DizzyStar
這裡顯示了 Niagara 效果的加載過程對系統造成了很大的負擔。
尤其是在 Niagara 的初次載入或初次生成時,可能會因為着色器編譯而導致卡頓現象。
這次我會解釋初次載入時出現卡頓的原因,不過如果是初次生成時出現卡頓的情況,可以通過事前(例如在黑屏過場等)在看不見的地方進行一次生成來解決。
在Package版本中,這種停頓只會在安裝後的第一次發生。
而在 UE 中,則僅在 UE 啟動後的第一次會發生。
如果想要重現停頓,則需要重啟 Unreal Engine 或刪除並重新安裝包。
這是造成問題的 Niagara 效果。
查看 C++ 代碼後,加載過程如下所示。
PlayerCharacter.h1public: 2 //... 3 UPROPERTY(EditAnywhere, BlueprintReadWrite) 4 TSoftObjectPtr<UNiagaraSystem> DizzyEffectAsset; 5 //...
PlayerCharacter.cpp1void APlayerCharacter::StartDizzy() 2{ 3 if (IsDizzy) 4 { 5 return; 6 } 7 CharacterMovementComponent->MaxWalkSpeed = DizzySpeed; 8 9 10 IsDizzy = true; 11 12 UNiagaraSystem* DizzyEffectSystem = DizzyEffectAsset.LoadSynchronous(); 13 if (!IsValid(DizzyEffectSystem)) 14 { 15 UE_LOG(LogTemp, Error, TEXT("DizzyEffectSystem is null, Function name: %s"), *FString(__FUNCTION__)); 16 } 17 DizzyEffect = UNiagaraFunctionLibrary::SpawnSystemAttached(DizzyEffectSystem, SceneComponent, NAME_None, 18 DizzyEffectOffset, FRotator::ZeroRotator, 19 EAttachLocation::KeepRelativeOffset, true); 20 21 if (!IsValid(DizzySoundAsset)) 22 { 23 UE_LOG(LogTemp, Error, TEXT("DizzySound is null, Function name: %s"), *FString(__FUNCTION__)); 24 } 25 else 26 { 27 DizzySound = UGameplayStatics::SpawnSoundAtLocation(GetWorld(), DizzySoundAsset, GetActorLocation()); 28 } 29 GetWorldTimerManager().SetTimer(DizzyTimerHandle, this, &APlayerCharacter::EndDizzy, DizzyDuration, false); 30}
在即將生成特效之前,使用 LoadSynchronous()
同步載入特效素材是造成卡頓 的原因。
LoadSynchronous()
(同步載入)會等待載入完成(停止其他處理)。這使得玩家會感覺到卡頓。
UE 官方建議使用非同步載入。
參考影片: Maximizing Your Game's Performance in Unreal Engine | Unreal Fest 2022
影片從 22:40 到 28:15。
27:33 時展示了如何在 Blueprint 中實現非同步載入(AsyncLoad)。
TSoftObjectPtr<UNiagaraSystem> DizzyEffectAsset
特效素材在玩家BP裏持有。
調查結果: 原因是生成特效之前的 LoadSynchronous()
。
實現非同步載入(AsyncLoad)
為了解決由同步載入造成的卡頓,我們會在遊戲開始時提前以非同步方式載入素材(預讀)。
非同步載入可能需要時間,所以如果確保載入的時間,生成素材的時候可能來不及。
接下來,我們將在 C++ 中創建一個用於非同步載入的函數。
PlayerCharacter.h1protected: 2 void OnDizzyEffectLoaded(); 3 void LoadDizzyEffectAsset();
PlayerCharacter.cpp1void APlayerCharacter::BeginPlay() 2{ 3 Super::BeginPlay(); 4 LoadDizzyEffectAsset(); 5} 6 7void APlayerCharacter::LoadDizzyEffectAsset() 8{ 9 UE_LOG(LogTemp, Log, TEXT("DizzyEffectAsset requeset load")); 10 UAssetManager::Get().GetStreamableManager().RequestAsyncLoad(DizzyEffectAsset.ToSoftObjectPath(), 11 FStreamableDelegate::CreateUObject( 12 this, &APlayerCharacter::OnDizzyEffectLoaded)); 13} 14 15void APlayerCharacter::OnDizzyEffectLoaded() 16{ 17 UE_LOG(LogTemp, Log, TEXT("DizzyEffectLoaded")); 18 19 if (IsValid(DizzyEffectAsset.Get())) 20 { 21 UE_LOG(LogTemp, Log, TEXT("DizzyEffectAsset is valid")); 22 } 23 else 24 { 25 UE_LOG(LogTemp, Error, TEXT("DizzyEffectAsset is null, Function name: %s"), *FString(__FUNCTION__)); 26 } 27}
接著,將素材的同步加載替換為 Get()
。
PlayerCharacter.cpp1void APlayerCharacter::StartDizzy() 2{ 3 if (IsDizzy) 4 { 5 return; 6 } 7 CharacterMovementComponent->MaxWalkSpeed = DizzySpeed; 8 9 10 IsDizzy = true; 11 12 UNiagaraSystem* DizzyEffectSystem = DizzyEffectAsset.Get(); 13 if (!IsValid(DizzyEffectSystem)) 14 { 15 UE_LOG(LogTemp, Error, TEXT("DizzyEffectSystem is null, Function name: %s"), *FString(__FUNCTION__)); 16 } 17 DizzyEffect = UNiagaraFunctionLibrary::SpawnSystemAttached(DizzyEffectSystem, SceneComponent, NAME_None, 18 DizzyEffectOffset, FRotator::ZeroRotator, 19 EAttachLocation::KeepRelativeOffset, true); 20 21 if (!IsValid(DizzySoundAsset)) 22 { 23 UE_LOG(LogTemp, Error, TEXT("DizzySound is null, Function name: %s"), *FString(__FUNCTION__)); 24 } 25 else 26 { 27 DizzySound = UGameplayStatics::SpawnSoundAtLocation(GetWorld(), DizzySoundAsset, GetActorLocation()); 28 } 29 GetWorldTimerManager().SetTimer(DizzyTimerHandle, this, &APlayerCharacter::EndDizzy, DizzyDuration, false); 30}
這樣非同步載入的實現就完成了。
接下來,我們在 Standalone 遊戲中進行確認。
如果Log顯示 DizzyEffectAsset is valid
,則可以確認素材的非同步載入(AsyncLoad)已成功。
結果
使用「stat UnitGraph」命令的結果顯示,圖表上的尖峰消失,卡頓問題得以解決。
總結
調查流程
- 卡頓的檢測:使用「stat unitgraph」命令將遊戲中發生的卡頓圖形化,並確定重負載發生的位置。
- Unreal Insight 的測量:為了調查卡頓發生時的詳細負載,使用 trace.start 和 trace.stop 命令收集追蹤數據,確認負載的來源。
問題的確定
- 特效的素材(Niagara 特效)因同步載入
LoadSynchronous()
而被載入,因此在載入完成之前,其他處理會被停止,導致卡頓的發生。
非同步載入的實現
- 為了避免卡頓,使用 TSoftObjectPtr 在遊戲開始時將特效非同步載入。這樣,玩家在使用特效之前,素材已經載入完成,從而避免卡頓。
結果
非同步載入的實現成功解決了卡頓問題,遊戲的流暢度得到了確認。通過使用非同步載入,我們提升了遊戲性能,改善了玩家的體驗。